# frozen_string_literal: true

fastlane_version '2.0'

default_platform :ios

# Paths that are re-used across multiple lanes
PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__))
FASTLANE_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'fastlane')
APP_STORE_METADATA_FOLDER = File.join(FASTLANE_FOLDER, 'metadata')
SECRETS_FOLDER = File.join(Dir.home, '.configure', 'pocketcasts-ios', 'secrets')
ASC_KEY_PATH = File.join(SECRETS_FOLDER, 'app_store_connect_fastlane_api_key.json')
VERSION_XCCONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.xcconfig')
SPM_CACHE_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'vendor', 'spm')
RESOURCES_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'podcasts', 'Resources')
RELEASE_NOTES_SOURCE_PATH = File.join(PROJECT_ROOT_FOLDER, 'CHANGELOG.md')
EXTRACTED_RELEASE_NOTES_PATH = File.join(RESOURCES_FOLDER, 'release_notes.txt')

GHHELPER_REPO = 'Automattic/pocket-casts-ios'

TEAM_ID = 'PZYM8XX95Q'

APP_STORE_VERSION_BUNDLE_IDENTIFIER = 'au.com.shiftyjelly.podcasts'
MAIN_BUNDLE_IDENTIFIERS = [
  APP_STORE_VERSION_BUNDLE_IDENTIFIER,
  'au.com.shiftyjelly.podcasts.NotificationExtension',
  'au.com.shiftyjelly.podcasts.NotificationContent',
  'au.com.shiftyjelly.podcasts.watchkitapp',
  'au.com.shiftyjelly.podcasts.watchkitapp.watchkitextension',
  'au.com.shiftyjelly.podcasts.PodcastsIntents',
  'au.com.shiftyjelly.podcasts.PodcastsIntentsUI',
  'au.com.shiftyjelly.podcasts.WidgetExtension'
].freeze

FROZEN_STRINGS_PATH = File.join(FASTLANE_FOLDER, 'Frozen.strings')

EN_LPROJ_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'podcasts', 'en.lproj')

# List of `.strings` files manually maintained by developers (as opposed to
# being automatically extracted from code and generated) which we will merge
# into the main `Localizable.strings` file imported by GlotPress, then extract
# back once we download the translations.
#
# Each `.strings` file to be merged/extracted is associated with a prefix to
# add to the keys, used to avoid conflicts and differentiate the source of the
# copies.
#
# See calls to `ios_merge_strings_files` and
# `ios_extract_keys_from_strings_files` for usage.
MANUALLY_MAINTAINED_STRINGS_FILES = {
  File.join(EN_LPROJ_FOLDER, 'Localizable.strings') => nil,
  File.join(EN_LPROJ_FOLDER, 'InfoPlist.strings') => 'infoplist_',
  File.join(EN_LPROJ_FOLDER, 'Intents.strings') => 'siri_intent_definition_key_'
}.freeze

# URL of the GlotPress project containing the strings used in the app
GLOTPRESS_APP_STRINGS_PROJECT_URL = 'https://translate.wordpress.com/projects/pocket-casts/ios/'
# URL of the GlotPress project containing App Store Connect metadata
GLOTPRESS_APP_STORE_METADATA_PROJECT_URL = 'https://translate.wordpress.com/projects/pocket-casts/ios/release-notes/'

# List of locales used for the app strings (GlotPress code => `*.lproj` folder name`).
# Sorted like Xcode sorts them in the File Inspector for easier comparison.
#
# TODO: Replace with `LocaleHelper` once provided by release toolkit (https://github.com/wordpress-mobile/release-toolkit/pull/296)
GLOTPRESS_TO_LPROJ_APP_LOCALE_CODES = {
  'zh-cn' => 'zh-Hans', # Chinese (China, Simplified)
  'zh-tw' => 'zh-Hant', # Chinese (Taiwan, Traditional)
  'nl' => 'nl',         # Dutch
  'fr' => 'fr',         # French
  'fr-ca' => 'fr-CA',   # French (Canadian)
  'de' => 'de',         # German
  'it' => 'it',         # Italian
  'ja' => 'ja',         # Japanese
  'pt-br' => 'pt-BR',   # Portuguese (Brazil)
  'ru' => 'ru',         # Russian
  'es' => 'es',         # Spanish
  'es-mx' => 'es-MX',   # Spanish (Mexico)
  'sv' => 'sv'          # Swedish
}.freeze

# Mapping of all locales which can be used for AppStore metadata
# (GlotPress code => AppStore Connect code)
#
# TODO: Replace with `LocaleHelper` once provided by release toolkit
# (https://github.com/wordpress-mobile/release-toolkit/pull/296)
GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES = {
  'de' => 'de-DE',
  'es' => 'es-ES',
  'fr' => 'fr-FR',
  'it' => 'it',
  'ja' => 'ja',
  'nl' => 'nl-NL',
  'pt-br' => 'pt-BR',
  'ru' => 'ru',
  'sv' => 'sv',
  'zh-cn' => 'zh-Hans',
  'zh-tw' => 'zh-Hant'
}.freeze

before_all do
  # This is necessary for `match` to work correctly in CI. When running
  # locally, it has no effect so it's safe to run it before all lanes.
  setup_ci

  # Decrypt the secrets. This is redundant on dev machines most of the time,
  # but it has such a negligible overhead that it's worth running it here to
  # keep the individual lanes cleaner.
  configure_apply
end

platform :ios do
  # Unfortunately, release toolkit still relies on certain settings being set in the environment
  ENV['PROJECT_NAME'] = 'podcasts'
  ENV['PROJECT_ROOT_FOLDER'] = "#{PROJECT_ROOT_FOLDER}/"
  ENV['PUBLIC_CONFIG_FILE'] = VERSION_XCCONFIG_PATH
  ENV['GHHELPER_REPO'] = GHHELPER_REPO

  ENV['FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT'] = '120'

  desc 'Run the unit tests'
  lane :test do
    load_ci_spm_cache

    run_tests(
      scheme: 'pocketcasts',
      cloned_source_packages_path: SPM_CACHE_FOLDER
    )

    save_ci_spm_cache
  end

  desc 'Submit a new Beta Build to Apple TestFlight'
  lane :beta do
    ENV['DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS'] = '-t Aspera'

    configure_code_signing

    gym(scheme: 'pocketcasts',
        include_bitcode: false,
        include_symbols: true,
        clean: true)

    secrets_dir = File.join(Dir.home, '.configure', 'pocketcasts-ios', 'secrets')
    pilot(skip_submission: true,
          api_key_path: File.join(secrets_dir, 'app_store_connect_fastlane_api_key.json'),
          skip_waiting_for_build_processing: true)

    sh 'cd .. && make upload_dsyms'
    clean_build_artifacts
    increment_version_on_config
  end

  desc 'Build the project only'
  lane :build do
    # CI has its own mechanism to setup the Ruby gems and CocoaPods
    unless is_ci
      bundle_install
      cocoapods
    end

    configure_code_signing
    load_ci_spm_cache

    gym(
      scheme: 'pocketcasts',
      include_bitcode: false,
      cloned_source_packages_path: SPM_CACHE_FOLDER,
      clean: true
    )
    clean_build_artifacts
    sh(command: 'rm -fr ~/Library/Developer/Xcode/Archives/*')

    save_ci_spm_cache
  end

  desc 'This lane downloads and configures the code signing certificates and profiles.'
  lane :configure_code_signing do
    match(
      type: 'appstore',
      team_id: TEAM_ID,
      api_key_path: ASC_KEY_PATH,
      app_identifier: MAIN_BUNDLE_IDENTIFIERS,
      # This might turn out to be useful in the future
      # template_name: 'CarPlay audio app (CarPlay + Media Player frameworks)iOS (Dist)'

      storage_mode: 'google_cloud',
      google_cloud_bucket_name: 'a8c-fastlane-match',
      google_cloud_keys_file: File.join(SECRETS_FOLDER, 'google_cloud_keys.json'),

      readonly: true
    )
  end

  # Increment the value of `VERSION_LONG` from the `Version.xcconfig` file
  #
  def increment_version_on_config
    fastlane_require 'Xcodeproj'

    # Get the current build version, and update it if needed
    versions = Xcodeproj::Config.new(File.new(VERSION_XCCONFIG_PATH)).to_hash
    build_number = versions['VERSION_LONG'].to_i + 1
    UI.message("Updating build version to #{build_number}")
    versions['VERSION_LONG'] = build_number
    new_config = Xcodeproj::Config.new(versions)
    new_config.save_as(Pathname.new(VERSION_XCCONFIG_PATH))
  end

  #####################################################################################
  # Release Lanes
  #####################################################################################

  # Executes the code freeze
  #
  # - Call start_code_freeze
  # - Call complete_code_freeze
  # - Go over PRs with the current milestone and add a comment to move them to the next
  # - Open a PR targeting trunk
  # - Send a message to Slack channel confirming the code freeze was finished
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  #
  desc 'Executes all the code freeze steps'
  lane :code_freeze do |options|
    start_code_freeze(options)
    comment_on_prs(milestone: ios_get_app_version)
    complete_code_freeze(options)
  end

  # Go through PRs and check their milestones, leaving a message if the target milestone is being frozen
  # rubocop:disable Metrics/MethodLength
  def comment_on_prs(milestone:)
    version = milestone
    UI.message("Checking open PRs pointing to #{version} milestone...")

    github_api(
      server_url: 'https://api.github.com',
      api_token: ENV.fetch('GITHUB_TOKEN'),
      http_method: 'GET',
      path: "/repos/#{GHHELPER_REPO}/pulls",
      error_handlers: {
        '*' => proc do |_result|
          UI.important('⚠️ Failed checking open PRs, skipping this step.')
        end
      }
    ) do |result|
      result[:json].each do |pr|
        next if pr['milestone'].nil?
        next unless pr['milestone']['title'].include?(version)

        comment_on_pr(
          project: GHHELPER_REPO,
          pr_number: pr['number'],
          body: "\nThis PR is targeting #{version} which is entering code freeze. If this needs to be included in the #{version} release please merge it into `release/#{version}` if not please move it to the next milestone.",
          reuse_identifier: 'code-freeze-in-progress'
        )
      end
    end
  end

  def slack_message(version:, build_number:, is_beta:)
    build_number_split = build_number.split('.')

    github_url = "https://github.com/#{GHHELPER_REPO}"
    message_root = lambda { |tag, display_name|
      ":announcement: <#{github_url}/releases/tag/#{tag}|*#{display_name}*>"
    }
    appstoreconnect_url = 'https://appstoreconnect.apple.com/apps/414834813'
    testflight_submit_link = "<#{appstoreconnect_url}/testflight/ios|App Store Connect>"
    appstore_submit_link = "<#{appstoreconnect_url}/versions/deliverable|App Store Connect>"
    merge_pr_link = "<https://github.com/Automattic/pocket-casts-ios/pulls?q=is%3Apr+is%3Aopen+#{build_number}+into|merge the PR>"

    if is_beta
      if (build_number_split[3] || '0') == '0'
        "#{message_root.call(build_number, version)} code freeze is completed.\nPlease submit #{build_number} for testers on #{testflight_submit_link} and #{merge_pr_link} (<https://wp.me/PdeCcb-1ju|need help?>)"
      else
        "#{message_root.call(build_number, build_number)} beta has been submitted to Apple.\nPlease distribute #{build_number} to testers on #{testflight_submit_link} and #{merge_pr_link} (<https://wp.me/PdeCcb-1ku|need help?>)"
      end
    elsif (build_number_split[2] || '0').to_i.positive?
      "#{message_root.call(version, version)} hotfix has been uploaded to Apple.\nPlease submit it for review on #{appstore_submit_link} and #{merge_pr_link}."
    else
      "#{message_root.call(version, version)} final build has been uploaded to Apple.\nPlease submit it for review on #{appstore_submit_link} and #{merge_pr_link}."
    end
  end
  # rubocop:enable Metrics/MethodLength

  # Executes the initial steps of the code freeze
  #
  # - Cuts a new release branch
  # - Extracts the Release Notes
  # - Freezes the GitHub milestone and enables the GitHub branch protection for the new branch
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  #
  desc 'Executes the initial steps needed during code freeze'
  lane :start_code_freeze do |options|
    ios_codefreeze_prechecks(options)

    ios_bump_version_release(skip_deliver: true, skip_glotpress: true)
    new_version = ios_get_app_version
    extract_release_notes_for_version(
      version: new_version,
      release_notes_file_path: RELEASE_NOTES_SOURCE_PATH,
      extracted_notes_file_path: EXTRACTED_RELEASE_NOTES_PATH
    )
    ios_update_release_notes(
      new_version: new_version,
      release_notes_file_path: RELEASE_NOTES_SOURCE_PATH
    )
    setbranchprotection(repository: GHHELPER_REPO, branch: release_branch_name)
    setfrozentag(repository: GHHELPER_REPO, milestone: new_version)
  end

  # Executes the final steps for the code freeze
  #
  #  - Generates `.strings` files from code then merges the other, manually-maintained `.strings` files with it
  #  - Triggers the build of the first beta on CI
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  #
  desc 'Completes the final steps for the code freeze'
  desc 'Creates a new release branch from the current trunk'
  lane :complete_code_freeze do |options|
    ios_completecodefreeze_prechecks(options)
    generate_strings_file_for_glotpress

    after_confirming_push do
      trigger_beta_build(branch_to_build: release_branch_name)
    end
  end

  def release_branch_name
    "release/#{ios_get_app_version}"
  end

  # Triggers a beta build on CI
  #
  # @option [String] branch_to_build The name of the branch we want the CI to build, e.g. `release/19.3`
  #
  lane :trigger_beta_build do |options|
    trigger_buildkite_release_build(branch: options[:branch_to_build], beta: true)
  end

  # Triggers a stable release build on CI
  #
  # @option [String] branch_to_build The name of the branch we want the CI to build, e.g. `release/19.3`
  #
  lane :trigger_release_build do |options|
    trigger_buildkite_release_build(branch: options[:branch_to_build], beta: false)
  end

  # Finalizes a release at the end of a sprint to submit to the App Store
  #
  #  - Updates store metadata
  #  - Bumps final version number
  #  - Removes branch protection and close milestone
  #  - Triggers the final release on CI
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  #
  desc 'Trigger the final release build on CI'
  lane :finalize_release do |options|
    if ios_current_branch_is_hotfix
      UI.user_error!('To finalize a hotfix, please use the finalize_hotfix_release lane instead')
    end

    ios_finalize_prechecks(options)
    check_all_translations_progress(interactive: true)

    download_localized_strings_and_metadata_from_glotpress
    lint_localizations
    ios_bump_version_beta

    # Wrap up
    version = ios_get_app_version
    removebranchprotection(repository: GHHELPER_REPO, branch: release_branch_name)
    setfrozentag(repository: GHHELPER_REPO, milestone: version, freeze: false)
    create_new_milestone(repository: GHHELPER_REPO)
    close_milestone(repository: GHHELPER_REPO, milestone: version)

    # Start the build
    trigger_release_build(branch_to_build: release_branch_name)
  end

  # Builds the Pocket Casts app and uploads it to TestFlight, for beta-testing or final release
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  # @option [Boolean] skip_prechecks (default: false) If true, don't run the ios_build_prechecks and ios_build_preflight
  # @option [Boolean] create_release If true, creates a GitHub Release draft after the upload, with zipped xcarchive as artefact
  # @option [Boolean] beta_release If true, the GitHub release will be marked as being a pre-release
  #
  # @called_by CI
  #
  desc 'Builds and uploads for distribution to App Store Connect'
  lane :build_and_upload_app_store_connect do |options|
    ios_build_prechecks(skip_confirm: options[:skip_confirm], external: true) unless options[:skip_prechecks]
    ios_build_preflight unless options[:skip_prechecks]

    configure_code_signing

    gym(
      scheme: 'pocketcasts',
      include_bitcode: false,
      include_symbols: true,
      clean: true,
      export_options: { method: 'app-store' }
    )

    secrets_dir = File.join(Dir.home, '.configure', 'pocketcasts-ios', 'secrets')
    testflight(
      skip_waiting_for_build_processing: true,
      team_id: TEAM_ID,
      api_key_path: File.join(secrets_dir, 'app_store_connect_fastlane_api_key.json')
    )

    sh 'cd .. && make upload_dsyms'

    next unless options[:create_release]

    archive_zip_path = File.join(PROJECT_ROOT_FOLDER, 'PocketCasts.xcarchive.zip')
    zip(path: lane_context[SharedValues::XCODEBUILD_ARCHIVE], output_path: archive_zip_path)

    version = options[:beta_release] ? ios_get_build_version : ios_get_app_version

    UI.message("Creating #{version} release on GitHub...")
    set_github_release(
      repository_name: GHHELPER_REPO,
      api_token: ENV.fetch('GITHUB_TOKEN'),
      name: version,
      tag_name: version,
      description: begin
        File.read(File.join(RESOURCES_FOLDER, 'release_notes.txt'))
      rescue StandardError
        'No changelog provided'
      end,
      commitish: Git.open(PROJECT_ROOT_FOLDER).log.first.sha,
      upload_assets: [archive_zip_path.to_s],
      is_draft: !options[:beta_release],
      is_prerelease: options[:beta_release]
    )

    # TODO: move this to a GH action
    UI.message('Opening PR to merge changes to trunk...')
    github_api(
      server_url: 'https://api.github.com',
      api_token: ENV.fetch('GITHUB_TOKEN'),
      http_method: 'POST',
      path: "/repos/#{GHHELPER_REPO}/pulls",
      body: {
        title: "#{ios_get_app_version} Release: Merge changes from #{ios_get_build_version} into `trunk`",
        body: "Merge changes from #{ios_get_app_version} (#{ios_get_build_version}) to `trunk`.\n\n## To test\n\n- Ensure the build is 🟢\n- The code changes here were tested in their own PRs",
        head: release_branch_name,
        base: 'trunk'
      },
      error_handlers: {
        '*' => proc do |_result|
          UI.important("⚠️ Couldn't open the PR to merge changes to trunk. Please do it manually.")
        end
      }
    )

    UI.message('Sending message to Slack...')
    slack(
      pretext: slack_message(version: ios_get_app_version, build_number: ios_get_build_version, is_beta: options[:beta_release]),
      default_payloads: [],
      slack_url: ENV.fetch('SLACK_WEBHOOK'),
      fail_on_error: false
    )

    FileUtils.rm_rf(archive_zip_path)
  end

  # Creates a new beta by bumping the app version appropriately then triggering a beta build on CI
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  # @option [String] base_version (default: _current app version_) If set, bases the beta on the specified version
  #                  and `release/<base_version>` branch instead of the current one. Useful for triggering betas on hotfixes for example.
  #
  desc 'Trigger a new beta build on CI'
  lane :new_beta_release do |options|
    ios_betabuild_prechecks(options)

    # Re-generate the strings for GlotPress, just in case there were
    # localization fixes.
    generate_strings_file_for_glotpress

    download_localized_strings_and_metadata_from_glotpress
    lint_localizations

    ios_bump_version_beta

    trigger_beta_build(branch_to_build: release_branch_name)
  end

  # Sets the stage to start working on a hotfix
  #
  # - Cuts a new `release/x.y.z` branch from the tag from the latest (`x.y`) version
  # - Bumps the app version numbers appropriately
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  # @option [String] version (required) The version number to use for the hotfix (`"x.y.z"`)
  #
  desc 'Creates a new hotfix branch for the given `version:x.y.z`. The branch will be cut from the `x.y` tag.'
  lane :new_hotfix_release do |options|
    prev_ver = ios_hotfix_prechecks(options)
    ios_bump_version_hotfix(previous_version: prev_ver, version: options[:version], skip_deliver: true)
  end

  # Finalizes a hotfix, by triggering a release build on CI
  #
  # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt
  #
  desc 'Performs the final checks and triggers a release build for the hotfix in the current branch'
  lane :finalize_hotfix_release do |options|
    # Push and trigger build
    after_confirming_push do
      ios_finalize_prechecks(options)
      trigger_release_build(branch_to_build: release_branch_name)
    end
  end

  # Generates the `.strings` file to be imported by GlotPress, by parsing source
  # code.
  #
  # @note Uses `genstrings` under the hood.
  # @called_by `complete_code_freeze`.
  #
  lane :generate_strings_file_for_glotpress do
    # For reference: Other apps run `cocoapods` here (equivalent to `bundle
    # exec pod install`) because they have internal libraries that bring in
    # their own strings. Pocket Casts does not have dependencies with strings
    # fetched via CocoaPods, so we don't need to waste time on that.

    # Delete the previous frozen `.strings` file before generating it again, to
    # avoid duplicated keys.
    FileUtils.rm(FROZEN_STRINGS_PATH)

    # Other apps call `ios_generate_strings_file_from_code` as the first step
    # of this process, but Pocket Casts iOS uses the convention of defining all
    # localized strings in the `en.lproj/Localizable.strings` file and then use
    # SwiftGen to generate reference to them for the code. With this approach,
    # there are no `NSLocalizedStrings` in the codebase and that action would
    # be useless.

    # Merge the various `.strings` files into a single "frozen" `.strings`
    # so that we can update all keys into a single GlotPress project.
    #
    # Note: We will re-extract the translations back during
    # `download_localized_strings_from_glotpress` (via a call to
    # `ios_extract_keys_from_strings_files`)
    ios_merge_strings_files(
      paths_to_merge: MANUALLY_MAINTAINED_STRINGS_FILES,
      destination: FROZEN_STRINGS_PATH
    )

    git_commit(
      path: FROZEN_STRINGS_PATH,
      message: 'Freeze strings for localization',
      allow_nothing_to_commit: true
    )
  end

  desc 'Downloads localized strings and App Store Connect metadata from GlotPress'
  lane :download_localized_strings_and_metadata_from_glotpress do
    download_localized_strings_and_metadata
    download_localized_app_store_metadata_from_glotpress
  end

  # Downloads the localized app strings and App Store Connect metadata from GlotPress.
  #
  desc 'Downloads localized metadata for App Store Connect from GlotPress'
  lane :download_localized_strings_and_metadata do
    # FIXME: This is a copy of what the release toolkit `ios_update_metadata` action does.
    # We'll soon replace this with the new `ios_download_strings_files_from_glotpress`-based workflow.
    sh("cd #{PROJECT_ROOT_FOLDER} && ./scripts/update-translations.rb")

    files_to_commit = Dir.glob(File.join(PROJECT_ROOT_FOLDER, 'podcasts', '**', '*.strings'))
    git_add(path: files_to_commit, shell_escape: false)
    git_commit(
      path: files_to_commit,
      message: 'Update translations',
      allow_nothing_to_commit: true
    )
  end

  desc 'Lint the `.strings` files'
  lane :lint_localizations do
    ios_lint_localizations(input_dir: File.join(PROJECT_ROOT_FOLDER, 'podcasts'), allow_retry: true)
  end

  # This lane updates the `AppStoreStrings.po` file for the Pocket Casts app
  # with the latest content from the `release_notes.txt` file and the other
  # text sources
  #
  desc 'Updates the `AppStoreStrings.po` file for the Pocket Casts app with the latest data'
  lane :update_app_store_strings do
    source_metadata_folder = File.join(APP_STORE_METADATA_FOLDER, 'default')
    version = get_version_number(xcodeproj: 'podcasts.xcodeproj', target: 'podcasts')

    files = {
      whats_new: File.join(source_metadata_folder, 'release_notes.txt'),
      app_store_subtitle: File.join(source_metadata_folder, 'subtitle.txt'),
      app_store_desc: File.join(source_metadata_folder, 'description.txt'),
      app_store_keywords: File.join(source_metadata_folder, 'keywords.txt')
    }

    ios_update_metadata_source(
      po_file_path: File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'AppStoreStrings.po'),
      source_files: files,
      release_version: version
    )
  end

  desc 'Downloads localized `.strings` from GlotPress'
  lane :download_localized_strings_from_glotpress do
    # Use the same name as the frozen strings source to make it explicit these
    # are not to be copied as-is in the `*.lproj/Localizable.strings`
    table_basename = basename_without_extension(path: FROZEN_STRINGS_PATH)

    # Notice that we don't need to track the files we'll download here in Git,
    # because the content they carry will be read and ported into the
    # appropriate individual localization files next.
    download_dir = File.join(FASTLANE_FOLDER, 'app-localization-downloads')
    ios_download_strings_files_from_glotpress(
      project_url: GLOTPRESS_APP_STRINGS_PROJECT_URL,
      locales: GLOTPRESS_TO_LPROJ_APP_LOCALE_CODES,
      download_dir: download_dir,
      table_basename: table_basename
    )

    # Redispatch the appropriate subset of translations back to the individual
    # `.strings` files that we merged via `ios_merge_strings_files` during
    # `complete_code_freeze`.
    modified_files = ios_extract_keys_from_strings_files(
      source_parent_dir: download_dir,
      source_tablename: table_basename,
      target_original_files: MANUALLY_MAINTAINED_STRINGS_FILES
    )
    git_commit(
      path: modified_files,
      message: 'Update localization files with up-to-date values from GlotPress',
      allow_nothing_to_commit: true
    )
  end

  desc 'Downloads localized metadata for App Store Connect from GlotPress'
  lane :download_localized_app_store_metadata_from_glotpress do
    # FIXME: Replace this with a call to the future replacement of
    # `gp_downloadmetadata` once it's implemented in the release-toolkit (see
    # paaHJt-31O-p2).
    target_files = {
      "v#{ios_get_app_version}-whats-new": {
        desc: 'release_notes.txt',
        max_size: 4000
      },
      app_store_subtitle: { desc: 'subtitle.txt', max_size: 30 },
      app_store_desc: { desc: 'description.txt', max_size: 4000 },
      app_store_keywords: { desc: 'keywords.txt', max_size: 100 }
    }

    gp_downloadmetadata(
      project_url: GLOTPRESS_APP_STORE_METADATA_PROJECT_URL,
      target_files: target_files,
      locales: GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES,
      download_path: APP_STORE_METADATA_FOLDER
    )
    files_to_commit = [File.join(APP_STORE_METADATA_FOLDER, '**', '*.txt')]

    # Ensure that none of the `.txt` files in `en-US` would accidentally
    # override our originals in `default`
    target_files.values.map { |h| h[:desc] }.each do |file|
      en_file_path = File.join(APP_STORE_METADATA_FOLDER, 'en-US', file)
      next unless File.exist?(en_file_path)

      UI.user_error!("File `#{en_file_path}` would override the same one in `#{APP_STORE_METADATA_FOLDER}/default
`, but `default/` is the source of truth. " \
+ "Delete the `#{en_file_path}` file, ensure the `default/` one has the expected orig
inal copy, and try again.")
    end

    # Ensure even empty locale folders have an empty `.gitkeep` file (in case
    # we don't have any translation at all ready for some locales)
    GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES.each_value do |locale|
      gitkeep = File.join(APP_STORE_METADATA_FOLDER, locale, '.gitkeep')
      next if File.exist?(gitkeep)

      FileUtils.mkdir_p(File.dirname(gitkeep))
      FileUtils.touch(gitkeep)
      files_to_commit.append(gitkeep)
    end

    # Commit
    git_add(path: files_to_commit, shell_escape: false)
    git_commit(
      path: files_to_commit,
      message: 'Update App Store metadata translations',
      allow_nothing_to_commit: true
    )
  end

  # Checks the translation progress (%) of all Mag16 for all the projects (app
  # strings and metadata) in GlotPress.
  #
  # @option [Boolean] interactive (default: false) If true, will pause and ask
  # confirmation to continue if it found any locale translated below the
  # threshold
  #
  desc 'Check translation progress for all GlotPress projects'
  lane :check_all_translations_progress do |options|
    abort_on_violations = false
    skip_confirm = options.fetch(:interactive, false) == false

    UI.header('Checking app strings translation status...')
    check_translation_progress(
      glotpress_url: GLOTPRESS_APP_STRINGS_PROJECT_URL,
      language_codes: GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES.keys,
      abort_on_violations: abort_on_violations,
      skip_confirm: skip_confirm
    )

    UI.header('Checking release notes strings translation status...')
    check_translation_progress(
      glotpress_url: GLOTPRESS_APP_STORE_METADATA_PROJECT_URL,
      language_codes: GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES.keys,
      abort_on_violations: abort_on_violations,
      skip_confirm: skip_confirm
    )
  end

  # Upload the localized metadata (from `fastlane/metadata/`) to App Store Connect
  #
  # @option [Boolean] with_screenshots (default: false) If true, will also upload the latest screenshot files to ASC
  #
  desc 'Upload the localized metadata to App Store Connect, optionally including screenshots.'
  lane :update_metadata_on_app_store_connect do |options|
    # Skip screenshots by default. The naming is "with" to make it clear that
    # callers need to opt-in to adding screenshots. The naming of the deliver
    # (upload_to_app_store) parameter, on the other hand, uses the skip verb.
    with_screenshots = options.fetch(:with_screenshots, false)
    skip_screenshots = with_screenshots == false

    upload_to_app_store(
      app_identifier: APP_STORE_VERSION_BUNDLE_IDENTIFIER,
      app_version: ios_get_app_version,
      skip_binary_upload: true,
      screenshots_path: File.join(FASTLANE_FOLDER, 'screenshots'),
      skip_screenshots: skip_screenshots,
      overwrite_screenshots: true, # won't have effect if `skip_screenshots` is true
      phased_release: true,
      precheck_include_in_app_purchases: false,
      api_key_path: ASC_KEY_PATH
    )
  end

  # -----------------------------------------------------------------------------------
  # Kicks off a Buildkite build
  # -----------------------------------------------------------------------------------
  def trigger_buildkite_release_build(branch:, beta:)
    buildkite_trigger_build(
      buildkite_organization: 'automattic',
      buildkite_pipeline: 'pocket-casts-ios',
      branch: branch,
      environment: { BETA_RELEASE: beta },
      pipeline_file: 'release-builds.yml'
    )
  end

  # Generates localized screenshots for the iPhone, and iPad.
  # Tests run in the simulator so be sure to make any necessary Podfile changes such as
  # converting to use google-cast-sdk-no-bluetooth-mock
  #
  desc 'Generates localized screenshots for the AppStore'
  lane :screenshots do
    iphone_devices = ['iPhone 12']
    ipad_devices = ['iPad (9th generation)']

    # Build once to speed up the other runs
    scan(
      workspace: 'podcasts.xcworkspace',
      scheme: 'Screenshot Automation',
      build_for_testing: true,
      clean: true,
      devices: iphone_devices + ipad_devices,
      reset_simulator: true
    )

    # iPhone Light Intertace Screens
    snapshot(
      derived_data_path: lane_context[SharedValues::SCAN_DERIVED_DATA_PATH],
      devices: iphone_devices,
      dark_mode: false,
      erase_simulator: true,
      testplan: 'ScreenshotAutomation_iPhone_Light_Interface'
    )

    # iPhone Dark Intertace Screens
    snapshot(
      derived_data_path: lane_context[SharedValues::SCAN_DERIVED_DATA_PATH],
      devices: iphone_devices,
      dark_mode: true,
      testplan: 'ScreenshotAutomation_iPhone_Dark_Interface'
    )

    # iPad Light Intertace Screens
    snapshot(
      derived_data_path: lane_context[SharedValues::SCAN_DERIVED_DATA_PATH],
      devices: ipad_devices,
      dark_mode: false,
      erase_simulator: true,
      testplan: 'ScreenshotAutomation_iPad_Light_Interface'
    )

    # iPhone Dark Intertace Screens
    snapshot(
      derived_data_path: lane_context[SharedValues::SCAN_DERIVED_DATA_PATH],
      devices: ipad_devices,
      dark_mode: true,
      testplan: 'ScreenshotAutomation_iPad_Dark_Interface'
    )
  end

  # Generates localized screenshots for the Apple Watch.
  # Tests run in the simulator so be sure to make any necessary Podfile changes such as
  # converting to use google-cast-sdk-no-bluetooth-mock
  #
  # Setup:
  # - Log into an account with Plus. Run the test iPhone_GenerateScreenshots.test_watchSetup
  # on a device that is connected to the watch mentioned in the test.
  # - Ensure the data syncs to the simulated watch. Mocking out the ApplicationContext from
  # the device can help ensure a consistent response.
  #
  desc 'Generates localized Watch screenshots for the AppStore'
  lane :watch_screenshots do
    watch_devices = ['Apple Watch Series 7 - 45mm']

    snapshot(
      scheme: 'Screenshot Automation Watch',
      devices: watch_devices,
      test_without_building: false
    )
  end

  desc 'Registers a device to the Apple Developer Portal and adds it to the appropriate provisioning profiles'
  lane :register_new_device do |options|
    device_name = UI.input('Device name (leave empty if already added in portal): ') if options[:device_name].nil?
    unless device_name.empty?
      device_id = UI.input('Device ID: ') if options[:device_id].nil?
      UI.message "Adding #{device_name} with ID #{device_id} to the Developer Portal"
      UI.message 'Also registering it with any provisioning profiles associated with the following bundle identifiers:'
      MAIN_BUNDLE_IDENTIFIERS.each do |identifier|
        puts "\t#{identifier}"
      end

      # Register the user's device
      register_device(
        name: device_name,
        udid: device_id,
        team_id: TEAM_ID,
        api_key_path: ASC_KEY_PATH
      )
    end

    # We're about to use `add_development_certificates_to_provisioning_profiles` and `add_all_devices_to_provisioning_profiles`.
    # These actions use Developer Portal APIs that don't yet support authentication via API key (-.-').
    # Let's preemptively ask for and set the email here to avoid being asked twice for it if not set.

    require 'credentials_manager'

    # If Fastlane cannot instantiate a user, it will ask the caller for the email.
    # Once we have it, we can set it as `FASTLANE_USER` in the environment (which has lifecycle limited to this call) so that the next commands will already have access to it.
    # Note that if the user is already available to `AccountManager`, setting it in the environment is redundant, but Fastlane doesn't provide a way to check it so we have to do it anyway.
    ENV['FASTLANE_USER'] = CredentialsManager::AccountManager.new.user

    # Add all development certificates to the provisioning profiles (just in case – this is an easy step to miss)
    add_development_certificates_to_provisioning_profiles(
      team_id: TEAM_ID,
      app_identifier: MAIN_BUNDLE_IDENTIFIERS
    )

    # Add all devices to the provisioning profiles
    add_all_devices_to_provisioning_profiles(
      team_id: TEAM_ID,
      app_identifier: MAIN_BUNDLE_IDENTIFIERS
    )
  end

  # Generates a HTML containing the libraries acknowledgments.
  #
  desc 'Generates a HTML with the list of used libraries and their licenses'
  lane :acknowledgments do
    require 'commonmarker'

    acknowledgements = 'Acknowledgments'
    markdown = File.read("#{PROJECT_ROOT_FOLDER}/podcasts/acknowledgements.md")
    rendered_html = CommonMarker.render_html(markdown, :DEFAULT)
    styled_html = "<head>
                       <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
                       <style>
                         body {
                           font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
                           Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
                           font-size: 16px;
                           color: #1a1a1a;
                           margin: 20px;
                         }
                        @media (prefers-color-scheme: dark) {
                         body {
                          background: #1a1a1a;
                          color: white;
                         }
                        }
                         pre {
                          white-space: pre-wrap;
                         }
                       </style>
                       <title>
                         #{acknowledgements}
                       </title>
                     </head>
                     <body>
                       #{rendered_html}
                     </body>"

    ## Remove the <h1>, since we've promoted it to <title>
    styled_html = styled_html.sub('<h1>Acknowledgements</h1>', '')

    ## The glog library's license contains a URL that does not wrap in the web view,
    ## leading to a large right-hand whitespace gutter.  Work around this by explicitly
    ## inserting a <br> in the HTML.  Use gsub juuust in case another one sneaks in later.
    styled_html = styled_html.gsub('p?hl=en#dR3YEbitojA/COPYING', 'p?hl=en#dR3YEbitojA/COPYING<br>')

    File.write("#{PROJECT_ROOT_FOLDER}/podcasts/acknowledgements.html", styled_html)
  end

  def load_ci_spm_cache
    # Handling SPM caching for CI here for now to avoid having two sources of
    # truth for what the cache key is. The alternative would be to call the
    # save and restore cache commands in the CI pipeline and then duplicate the
    # logic to get the key here.
    sh(command: %(restore_cache "#{ci_spm_cache_key}")) if is_ci
  end

  def save_ci_spm_cache
    sh(command: %(save_cache #{SPM_CACHE_FOLDER} "#{ci_spm_cache_key}")) if is_ci
  end

  def ci_spm_cache_key
    UI.user_error! 'This function should be called from the Buildkite CI only!' unless is_ci

    hash = sh(command: 'hash_file "../podcasts.xcworkspace/xcshareddata/swiftpm/Package.resolved"').rstrip
    "$BUILDKITE_PIPELINE_SLUG-spm-cache-#{hash}"
  end

  def after_confirming_push(message: 'Push changes to the remote and trigger the build?')
    if ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', false) || UI.confirm(message)
      push_to_git_remote(tags: false)
      yield
    else
      UI.message('Aborting push as requested.')
    end
  end

  def basename_without_extension(path:)
    File.basename(path, '.*')
  end
end
